Skip to content

A Brief Discussion on Exception Handling and State Restoration for SaveChanges() in Entity Framework

TLDR

  • After SaveChanges() fails, the state of the Entities is preserved, causing subsequent write operations to include the previously failed changes, which leads to a chain reaction of failures.
  • It is recommended to override SaveChanges() and SaveChangesAsync() in DbContext, using ChangeTracker to catch DbUpdateException and reset the Entity state.
  • For Entities in the Modified state, use entry.CurrentValues.SetValues(entry.OriginalValues) to restore data and change the state to Unchanged.
  • For Entities in the Added state, change the state to Detached.
  • In complex structures involving foreign key relationships or navigation properties, restoring Entity state may lead to cache inconsistency; this restoration mechanism is not recommended in such scenarios.
  • InvalidOperationException occurring during the DbSet.Add() phase (e.g., primary key conflicts) will not be caught by the SaveChanges() error handling mechanism.

Common Exceptions in Entity Framework

During development, understanding the exception types thrown by EF helps in handling errors correctly:

  • DbUpdateException: Thrown when an error occurs while saving to the database (e.g., violation of database constraints, connection interruption). This exception usually encapsulates the underlying SQL execution error.
  • DbUpdateConcurrencyException: Thrown when a concurrency conflict occurs (e.g., RowVersion or ConcurrencyCheck is set, but the data in the database has been modified by someone else).
  • DbEntityValidationException: A validation exception from older versions of EF, which has been removed in EF Core. It is recommended to use Model Binding or a Service Layer for data validation.

Recommendations for Error Message Handling

When do you encounter the issue of overly generic error messages? When the system returns the raw Exception message directly to the frontend.

  • If you need to hide details from the outside: You should log the full information of the InnerException and return only a generic error message to the frontend.
  • If you do not need to hide details: You can override SaveChanges() in DbContext, catch the exception, and re-throw a new Exception containing the full error message to facilitate clear accountability.

State Restoration When SaveChanges() Fails

When do you encounter state restoration issues? When developers rely on database primary keys to block duplicate data and do not clear the changes in ChangeTracker after SaveChanges() fails.

Since EF preserves the failed Entity state, if the first SaveChanges() fails, subsequent write operations will still attempt to send the failed data to the database, causing all subsequent operations to fail. If you wish to ignore the changes after a failure, you can manually restore the state by overriding SaveChanges().

Implementation

The following is an extension implementation for DbContext used to automatically reset states when a DbUpdateException occurs:

csharp
public partial class TestEFContext {
    public override int SaveChanges() {
        return SaveChanges(true);
    }

    public override int SaveChanges(bool acceptAllChangesOnSuccess) {
        try {
            return base.SaveChanges(acceptAllChangesOnSuccess);
        } catch (DbUpdateException ex) {
            throw ResetEntityStateAndFixMessage(ex);
        }
    }

    public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) {
        return SaveChangesAsync(true, cancellationToken);
    }

    public override async Task<int> SaveChangesAsync(
        bool acceptAllChangesOnSuccess,
        CancellationToken cancellationToken = default
    ) {
        try {
            return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
        } catch (DbUpdateException ex) {
            throw ResetEntityStateAndFixMessage(ex);
        }
    }

    private DbUpdateException ResetEntityStateAndFixMessage(DbUpdateException ex) {
        ResetEntityStates(ChangeTracker.Entries());
        return new DbUpdateException(ex.InnerException.Message, ex);
    }

    private static void ResetEntityStates(IEnumerable<EntityEntry> entries) {
        foreach (EntityEntry entry in entries) {
            ResetEntityState(entry);
        }
    }

    private static void ResetEntityState(EntityEntry entry) {
        switch (entry.State) {
            case EntityState.Added:
                entry.State = EntityState.Detached;
                break;
            case EntityState.Modified:
                entry.CurrentValues.SetValues(entry.OriginalValues);
                entry.State = EntityState.Unchanged;
                break;
            case EntityState.Deleted:
                entry.State = entry.Entity is Dictionary<string, object>
                    ? EntityState.Detached
                    : EntityState.Unchanged;
                break;
        }
    }
}

WARNING

When using DbSet.Add() to add an Entity that has the same PK as data already queried, an InvalidOperationException will be thrown. Since the exception is thrown during Add() rather than SaveChanges(), it will not be caught by the error handling mechanism mentioned above.

Notes on Foreign Key Relationships

When do you encounter restoration failure issues? When the Entity structure contains complex navigation properties or foreign key relationships.

Tests have shown that if a related Entity in the EntityState.Deleted state is set to Unchanged, it may cause the DbContext caching mechanism to return an incorrect navigation property state. Although setting it to Detached can solve some problems, in general, there is still a potential risk of cache inconsistency when manually restoring Entity states in complex structures involving foreign key relationships.

TIP

A complete executable example for this article: CloudyWing/EfCoreBehaviorSample.


Change Log

  • 2024-08-17 Initial version created.
  • 2026-05-29 Added link to the corresponding GitHub sample project.